package server;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;

import common.LongcatServerInterface;
import common.NetLocation;
import common.PackageID;
import common.ServerListResult;
import common.StorageInterface;
import common.ServerGetResult;
import common.ServerStoreResult;

/**
 * Class represents an longcat server object.
 * This class implements all methods needed for full functionality. 
 * According to the decentralized architecture of longcat application 
 * server is intended to implement as little functionality as possible. 
 * Our goal was to degrade the role of the server as much as possible. 
 * Server handles only the data-base part of the application. 
 * All the data is transferred between clients. No storage data passes through server. 
 */
public class LongcatServer extends UnicastRemoteObject implements LongcatServerInterface {
	protected LongcatServer() throws RemoteException {
		super();
	}

	/**
	 * A subclass representing a server's point of view on the client.
	 * It gathers only the information necessary for the server application to possess.
	 * This kind of minimalism is consistent with our goal: to eliminate the role of the server as much as possible. 
	 * According to the decentralized architecture of longcat application 
	 * server is intended to implement as little functionality as possible. 
	 * Our goal was to degrade the role of the server to the minimum. 
	 * Server handles only the data-base part of the application. 
	 * All the data is transferred between clients. No storage data passes through server. 
	 */
	private class Client {
		/**
		 * Field governing all methods the client instance makes available for the server.
		 */
		public final StorageInterface storage;
		/**
		 * A port on which clients communicates with server. 
		 */
		public int port;
		/**
		 * 
		 */
		public Inet4Address address;
		public Client(StorageInterface storage) {
			this.storage = storage;
		}
		public NetLocation location() { 
			return new NetLocation(address, port);
		}
	}
	/**
	 * A subclass representing a server's look at the file on distributed storage.
	 * This could be thought of as of a virtual file. The distributed nature of a file is hidden from a user. 
	 * According to the decentralized architecture of longcat application 
	 * server is intended to implement as little functionality as possible. 
	 * Our goal was to degrade the role of the server to the minimum. 
	 * Server handles only the data-base part of the application. 
	 * All the data is transferred between clients. No storage data passes through server.  
	 */
	private static class File {
		/**
		 * Files unique identification number.
		 * The identification number is unique in scope of a server.
		 * For now no two files registered in one longcat server are mentioned to have the same identification number.
		 */
		public int id;
		/**
		 * The size of a distributed file on storage.
		 */
		public long size;
		/**
		 * A size of individual package. At this point of development the size of a package is constant.
		 */
		public int packageSize;
		/**
		 * A function that evaluates number of packages that the file consists of. 
		 * It bases on package size and file size. 
		 * @return
		 */
		public int packageCount(){//TODO: calls OK (server interface)
			return (int)size/packageSize + ((size%packageSize==0)?0:1);
		}
	}

	private HashMap<StorageInterface,Client> clients = new HashMap<StorageInterface,Client>();
	private HashMap<String, File> files = new HashMap<String, File>();
	private HashMap<PackageID,Set<StorageInterface>> packages = new HashMap<PackageID,Set<StorageInterface>>();
	private Random random = new Random(); 
	
	public static void main(String[] args) throws Exception {
		if(System.getSecurityManager() == null) {
			System.setSecurityManager (new RMISecurityManager() );
		}
		LongcatServer server;
		int port = 6785;
		try {
			server = new LongcatServer();
			if(args.length > 0) port = Integer.parseInt(args[0]);
            Registry registry = LocateRegistry.createRegistry(port);
            registry.bind("LongcatServerInterface", server);
            //creates a link in a registry so that when users call methods on LongcatServerInterface
            //it is called on instance "stub".
		} catch(Exception e) {
			System.out.println("The server encountered an error: " + e);
			e.printStackTrace();
			throw e;
		} 

		System.out.println("LongcatServer running at port " + port);
		//LongcatServerInterface remoteServer;
		//remoteServer = (LongcatServerInterface) Naming.lookup("//localhost:6785/LongcatServerInterface");
		//System.in.read();
		//remoteServer.ok("Hello");
		//System.out.println("Press any button to close...");
		Thread.sleep(1000000);
		System.out.println("LongcatServer closing...");
		System.exit(0);
	}
	
	public void delete(String remotePath){
		int fileID = files.get(remotePath).id; 
		files.remove(remotePath);
		for(Map.Entry<PackageID,Set<StorageInterface>> entry: packages.entrySet()){
			if(entry.getKey().fileID==fileID){
				for(StorageInterface subentry: entry.getValue()){
					try {
						subentry.removePackage(entry.getKey());
					} catch (RemoteException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				packages.remove(entry.getKey());
			}
		}
	}
	
	@Override
	public void registerStorage(StorageInterface storage, int port) {
		// TODO: store port
		try {
			storage.noop();
			Client client = new Client(storage);
			client.port = port;
			String host = getClientHost();
			client.address = (Inet4Address)Inet4Address.getByName(host);
			clients.put(storage, client);
			System.out.println("registerStorage successful, " + client.address +" " + client.port);
			
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ServerNotActiveException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
		public void disconnect(StorageInterface storage){
		NetLocation location = clients.get(storage).location();
		clients.remove(location);//delete client from register
		for(Map.Entry<PackageID,Set<StorageInterface>> entry: packages.entrySet()){			
			for(StorageInterface subentry: entry.getValue()){
				if(clients.get(subentry).location().isEqual(location)){
					packages.get(entry.getKey()).remove(storage);//delete lost package from register
					//TODO: does above line do the thing?
					StorageInterface to  = randomClient();
					StorageInterface from = null;
					while(from.equals(to))
						from = randomClient(entry.getKey());
					try {
						to.obtainPackage(entry.getKey(), clients.get(from).location());//clone lost package
					} catch (RemoteException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	@Override
	public ServerListResult list(){
		Collection<String> result = new ArrayList<String>();
		Collection<String> c = files.keySet();
		Iterator<String> itr = c.iterator();
		while(itr.hasNext())
			result.add(itr.next());
		return new ServerListResult(result);
	}
	public ServerGetResult get(String path) {
		int fileID = files.get(path).id;
		int packageCount = files.get(path).packageCount();
		Map<Integer,Collection<NetLocation>> partLocations = new HashMap<Integer,Collection<NetLocation>>();
		for(Map.Entry<PackageID,Set<StorageInterface>> entry: packages.entrySet()){
			if(entry.getKey().fileID == fileID){
				Collection<StorageInterface> interfaces = entry.getValue();
				Collection<NetLocation> register = new HashSet<NetLocation>();
				for(StorageInterface subentry: interfaces){
					Inet4Address address = clients.get(subentry).address;
					int port = clients.get(subentry).port;
					register.add(new NetLocation(address, port));
				}
				partLocations.put(entry.getKey().partID, register);
			}
		}
		ServerGetResult result = new ServerGetResult(fileID, packageCount, partLocations);
		return result;
	}
	
	private StorageInterface randomClient() {
		StorageInterface[] l = clients.keySet().toArray(new StorageInterface[0]);
		return l[random.nextInt(l.length)];
	}
	private StorageInterface randomClient(PackageID id) {
		StorageInterface[] l = packages.get(id).toArray(new StorageInterface[0]);
		return l[random.nextInt(l.length)];
	}
	@Override
	public void ok(StorageInterface storage, String path) throws RemoteException{
		File file = files.get(path);
		NetLocation location = clients.get(storage).location();
		for(int i = 1; i <= file.packageCount(); i++) {
			PackageID cur = new PackageID(file.id, i);
			Set<StorageInterface> c = packages.get(cur);
			c.add(storage);
			while(c.size() < 3 && clients.size() >= 3) {
				c.add(randomClient());
			}
			for(StorageInterface j : c) {
				if(!c.equals(storage)) j.obtainPackage(cur, location);
			}
		}
		System.out.println("ok " + path);
		//System.out.println(files);
		//System.out.println(packages);
	}

	@Override
	public ServerStoreResult store(String path, long fileSize) {
		Set<PackageID> justKeys = packages.keySet();
        PackageID[] keys = justKeys.toArray(new PackageID[justKeys.size()]);
        int[] ids = new int[justKeys.size()];
        for(int i=0; i<justKeys.size(); i++) {
            ids[i]=keys[i].fileID;
        }
        Arrays.sort(ids);
        int minimumExcluded = justKeys.size();//jesli ten numer jest zajęty to znaczy, 
        for(int i=0; i<justKeys.size(); i++) {//ze gdzies wczesniej jest dziura i ja znajdziemy
        	if(i!=ids[i]){
        		minimumExcluded = i;
        		break;
        	}	
        }
        
        File file = new File();
        file.packageSize = 100;
        file.size = fileSize; 
        long packageCount = file.packageCount();
        for(int i = 0; i < packageCount; i++) {
        	packages.put(new PackageID(minimumExcluded, i + 1), new HashSet<StorageInterface>());
        }
        files.put(path, file);
        System.out.println("store " + path);
        return (new ServerStoreResult(minimumExcluded, 100)); //do testowania wielkość pliku 100B
	}
}
